package de.mmis.core.base.gui;

import java.awt.BorderLayout;
import java.awt.Component;
import java.awt.Insets;
import java.util.Vector;

import javax.swing.JTable;
import javax.swing.JTextArea;
import javax.swing.table.AbstractTableModel;
import javax.swing.table.TableCellRenderer;
import javax.swing.table.TableRowSorter;

import de.mmis.core.base.event.Event;
import de.mmis.core.base.event.Observable;
import de.mmis.core.base.event.Observer;

/**
 * Abstract control component to display given items in a table. <br>
 * The data for the table has to be extracted from the received events of the
 * observed device.
 * 
 * @author Martin Dyrba
 * @param <D>
 *            type of device that is represented and/or controlled
 * @param <E>
 *            the event type of the device to listen to
 */
public abstract class AbstractTableControlComponent<D extends Observable<E>, E extends Event>
		extends AbstractControlComponent<D> {

	/**
	 * Cell renderer that provides line wrapping.
	 * 
	 * @author Martin Dyrba
	 */
	protected class MyCellRenderer extends JTextArea implements
			TableCellRenderer {
		private static final long serialVersionUID = 1L;

		/** Constructor. */
		public MyCellRenderer() {
			setLineWrap(true);
			setWrapStyleWord(true);
			setMargin(new Insets(0, 5, 0, 5));
			setDoubleBuffered(true);
		}

		@Override
		public Component getTableCellRendererComponent(JTable table,
				Object value, boolean isSelected, boolean hasFocus, int row,
				int column) {

			if (isSelected) {
				setForeground(table.getSelectionForeground());
				setBackground(table.getSelectionBackground());
			} else {
				setForeground(table.getForeground());
				setBackground(table.getBackground());
			}
			setText(value.toString());
			setSize(table.getColumnModel().getColumn(column).getWidth(),
					getPreferredSize().height);
			// expand table row height if not small enough
			if (table.getRowHeight(row) < getPreferredSize().height) {
				table.setRowHeight(row, getPreferredSize().height);
			}
			return this;
		}
	}

	/**
	 * Table model that provides a customized
	 * {@link MyTableModel#setData(Vector)} method to avoid flickering.<br>
	 * The cells of the table will be read-only.
	 * 
	 * @author Martin Dyrba
	 */
	protected class MyTableModel extends AbstractTableModel {
		private static final long serialVersionUID = 1L;

		protected Vector<Vector<Object>> data;
		protected Vector<String> columnNames;

		/**
		 * Constructor.
		 * 
		 * @param initialData
		 *            the initial data
		 * @param columnNames
		 *            the column names
		 */
		public MyTableModel(Vector<Vector<Object>> initialData,
				Vector<String> columnNames) {
			this.data = initialData;
			this.columnNames = columnNames;
		}

		@Override
		public int getRowCount() {
			return data.size();
		}

		@Override
		public int getColumnCount() {
			return columnNames.size();
		}

		@Override
		public Object getValueAt(int rowIndex, int columnIndex) {
			Vector<Object> row = data.get(rowIndex);
			return row.get(columnIndex);
		}

		@Override
		public String getColumnName(int column) {
			return columnNames.elementAt(column);
		}

		/**
		 * Sets the new data.<br>
		 * <b>Note:</b> It is assumed that the data has the same number of
		 * columns as the number of column names.
		 * 
		 * @param newData
		 *            the new content data of the table
		 */
		public void setData(Vector<Vector<Object>> newData) {
			data = newData;
			fireTableDataChanged();
		}
	}

	private static final long serialVersionUID = 1L;

	protected final Vector<String> colNames;
	protected final Vector<Vector<Object>> emptyData;

	protected JTable table;
	protected MyTableModel tableModel;

	/**
	 * Constructor.
	 * 
	 * @param defaultDisplayName
	 *            the control component display name
	 * @param colNames
	 *            the column names of the table
	 */
	public AbstractTableControlComponent(String defaultDisplayName,
			Class<? super D> applicableClass, Vector<String> colNames) {
		super(100, applicableClass, defaultDisplayName);

		this.colNames = colNames;

		emptyData = new Vector<Vector<Object>>();
	}

	@Override
	public void initialize(Class<?> clazz) {
		tableModel = new MyTableModel(emptyData, colNames);
		table = new JTable(tableModel);
		// table.setAutoCreateRowSorter(true); --> is reset every data update
		table.setFillsViewportHeight(true);
		table.setDoubleBuffered(true);
		table.setDefaultRenderer(Object.class, new MyCellRenderer());
		table.setRowSorter(new TableRowSorter<MyTableModel>(tableModel));

		getDevice().getDevice().addObserver(new Observer<E>() {
			@Override
			public void notify(Observable<? extends E> sender, E event) {

				// safe current selection
				int sel = table.getSelectedRow();

				Vector<Vector<Object>> newData = updateData(event);
				tableModel.setData(newData);

				// restore selection
				if (sel != -1 && sel < tableModel.getRowCount())
					table.setRowSelectionInterval(sel, sel);
			}
		});
		setLayout(new BorderLayout());
		add(table.getTableHeader(), BorderLayout.PAGE_START);
		add(table, BorderLayout.CENTER);

		Vector<Vector<Object>> newData = initializeData();
		tableModel.setData(newData);
	}

	/**
	 * Generates the new content data for the displayed table.<br>
	 * <b>Note:</b> It is assumed that the data has the same number of columns
	 * as the number of column names.
	 * 
	 * @param event
	 *            the behavior controller event containing the new data
	 * @return the new content data for the table (first vector are the rows,
	 *         second vector are the columns), or an empty vector for no
	 *         entries, never <code>null</code>!
	 */
	protected abstract Vector<Vector<Object>> updateData(E event);

	/**
	 * Generates the initial content data for the displayed table.<br>
	 * <b>Note:</b> It is assumed that the data has the same number of columns
	 * as the number of column names.
	 * 
	 * @return the new content data for the table (first vector are the rows,
	 *         second vector are the columns), or an empty vector for no
	 *         entries, never <code>null</code>!
	 */
	protected abstract Vector<Vector<Object>> initializeData();

	@Override
	public void notify(
			Observable<? extends de.mmis.core.base.gui.GUIComponent.GUIEvent> sender,
			de.mmis.core.base.gui.GUIComponent.GUIEvent event) {
		// do nothing
	}
}
